// Copyright (c) Appeon. All rights reserved.
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 
// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.

using System;
using SnapObjects.Commons;

namespace SnapObjects.Expressions
{
    internal abstract class TypeSynchronizer<T>
    {
        protected abstract Type GetType(T node);
        protected abstract T Convert(T node, Type type);
        protected abstract T ToString(T node);

        public void SynchronizeType(
            ref T left, ref T right, bool isStrict = false, ParserOptions<T> options = null)
        {
            var leftType = this.GetType(left);
            var rightType = this.GetType(right);

            if (leftType == typeof(object) && options != null)
            {
                if (options.StatementTypes.TryGetValue(left, out var type))
                {
                    leftType = type;

                    left = this.Convert(left, type);
                }
            }

            if (rightType == typeof(object) && options != null)
            {
                if (options.StatementTypes.TryGetValue(right, out var type))
                {
                    rightType = type;

                    right = this.Convert(right, type);
                }
            }

            if (leftType != rightType)
            {
                if (leftType == typeof(string))
                {
                    if (isStrict && rightType != typeof(char) && rightType != typeof(object))
                    {
                        throw new TypeMatchException(leftType, rightType);
                    }

                    right = this.ToString(right);
                }
                else if (rightType == typeof(string))
                {
                    if (isStrict && rightType != typeof(char) && rightType != typeof(object))
                    {
                        throw new TypeMatchException(leftType, rightType);
                    }

                    left = this.ToString(left);
                }
                else
                {
                    var type = this.GetPriorityType(leftType, rightType, isStrict);

                    if (type == null)
                    {
                        throw new TypeMatchException(leftType, rightType);
                    }

                    if (type != leftType)
                    {
                        left = this.Convert(left, type);
                    }
                    if (type != rightType)
                    {
                        right = this.Convert(right, type);
                    }
                }
            }
        }

        protected Type GetPriorityType(Type left, Type right, bool isStrict)
        {
            var nullable = false;

            if (left.IsNullable())
            {
                nullable = nullable || true;

                left = Nullable.GetUnderlyingType(left);
            }

            if (right.IsNullable())
            {
                nullable = nullable || true;

                right = Nullable.GetUnderlyingType(right);
            }

            var lcode = Type.GetTypeCode(left);
            var rcode = Type.GetTypeCode(right);

            if (lcode >= TypeCode.SByte && lcode <= TypeCode.UInt64 &&
                rcode >= TypeCode.Single && rcode <= TypeCode.Decimal)
            {
                return GetReturnType(right, nullable);
            }
            else if (rcode >= TypeCode.SByte && rcode <= TypeCode.UInt64
                 && lcode >= TypeCode.Single && lcode <= TypeCode.Decimal)
            {
                return GetReturnType(left, nullable);
            }
            else if (rcode == TypeCode.Boolean && lcode >= TypeCode.SByte && lcode <= TypeCode.Decimal)
            {
                return GetReturnType(left, nullable);
            }
            else if (lcode == TypeCode.Boolean && rcode >= TypeCode.SByte && rcode <= TypeCode.Decimal)
            {
                return GetReturnType(right, nullable);
            }
            else if (lcode >= TypeCode.SByte && lcode <= TypeCode.Decimal
                && rcode >= TypeCode.SByte && rcode <= TypeCode.Decimal)
            {
                return lcode < rcode
                    ? GetReturnType(right, nullable)
                    : GetReturnType(left, nullable);
            }

            if (lcode == TypeCode.Object)
            {
                return GetReturnType(right, nullable);
            }

            if (rcode == TypeCode.Object)
            {
                return GetReturnType(left, nullable);
            }

            if (isStrict)
            {
                return null;
            }

            return lcode < rcode
                ? GetReturnType(right, nullable)
                : GetReturnType(left, nullable);
        }

        private static Type GetReturnType(Type type, bool nullable)
        {
            return nullable ? typeof(Nullable<>).MakeGenericType(type) : type;
        }
    }
}
